package cz.cuni.mff.ufal.httpserver;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import org.apache.log4j.Logger;

/**
 * A simple HTTP server.
 *
 * @author Bartłomiej Etenkowski
 */
public class HttpServer {

	public HttpServer(HttpRequestHandler requestHandler) {
		if (requestHandler == null)
			throw new NullPointerException("The requestHandler argument is null");
		this.requestHandler = requestHandler;
	}

	private Logger log = Logger.getLogger(HttpServer.class);

	private volatile State state = State.STOPPED;

	// [start] running the server

	private Thread dispatcher;
	private ExecutorService requestProcessors;

	public synchronized void start() throws IOException {
		if (state != State.STOPPED) {
			throw new IllegalStateException(
			    "An attempt to run the HTTP server in a wrong state was made");
		}

		state = State.STARTING;

		requestProcessors = new ScheduledThreadPoolExecutor(threadCount,
		    new ThreadPoolExecutor.AbortPolicy());
		serverSocket = new ServerSocket(port);
		keepRunning = true;
		dispatcher = new Thread(new HttpServerWorker(this));
		dispatcher.start();

		state = State.RUNNING;
		log.info("The server is ready to accept incoming connections on port " + port);
	}

	/**
	 * Stops the server. It will wait for all the threads that are currently
	 * processing requests to terminate. Notice that this method can be called
	 * only when the server is running.
	 *
	 * <p>
	 * The method is thread safe.
	 * </p>
	 *
	 * @throws IllegalStateException
	 *           if the server is in another state than {@code State.RUNNING}
	 */
	public synchronized void stop() {
		if (state != State.RUNNING) {
			throw new IllegalStateException(
			    "An attempt to stop the HTTP server in a wrong state was made");
		}

		state = State.STOPPING;

		try {
			keepRunning = false;
			serverSocket.close();
			serverSocket = null;
			requestProcessors.shutdown();

			try {
				requestProcessors.awaitTermination(1000000, TimeUnit.DAYS);
				dispatcher.join();
			} catch (InterruptedException e) {
				log.error("An InterruptedException occurred", e);
			}
			dispatcher = null;
			state = State.STOPPED;
		} catch (IOException e) {
			log.error("An IOException occurred", e);
			state = State.ERROR;
		}
	}

	// [end]

	/** @return Current server state. */
	public State getState() {
		return state;
	}

	private int port = 8081; // TODO

	/** @return The port number which the server is running (or can be run) on. */
	public int getPort() {
		return port;
	}

	public synchronized void setPort(int value) {
		if (state != State.STOPPED)
			throw new IllegalStateException(
			    "Attempt to set the port service in a wrong state");
		port = value;
	}

	private int threadCount = 5;

	public int getThreadCount() {
		return threadCount;
	}

	public synchronized void setThreadCount(int threadCount) {
		if (state != State.STOPPED) {
			throw new IllegalStateException("An attempt to change the thread count in an invalid state was made");
		}
		this.threadCount = threadCount;
	}

	private volatile boolean keepRunning = false;

	private ServerSocket serverSocket;

	private HttpRequestHandler requestHandler;

	public HttpRequestHandler getRequestHandler() {
		return requestHandler;
	}

	public void setRequestHandler(HttpRequestHandler handler) {
		handler.getClass();
		requestHandler = handler;
	}

	private static class HttpServerWorker implements Runnable {

		HttpServerWorker(HttpServer server) {
			this.server = server;
		}

		final HttpServer server;

		@Override
		public void run() {
			try {
				while (server.keepRunning) {
					try {
						server.log.trace("Waiting for an incoming connection");
						Socket clientSocket = server.serverSocket.accept();
						server.log.trace("Incoming connection accepted, enqueuing");
						try {
							server.requestProcessors.execute(new HttpServerRequestHandler(
							    clientSocket, server.requestHandler));
						} catch (RejectedExecutionException e) {
							server.log.error("The thread pool rejected to execute the query");
						}
					} catch (SocketException e) {
						server.log.info("Socket closed");
					}
				}
			} catch (IOException e) {
				server.state = State.ERROR;
				server.log.error("An IOException occurred", e);
			}
		}

	}
}
